繼續來探究 Active Record,前幾篇談論資料的關聯性,這次我們來點在 Active Record 怎麼抓取資料!
大家還記得當時提及 Active Record 的好處嗎?
如果沒印象,可以回頭看看:Day 19 - 理解 Ruby on Rails,ORM 與 Active Record 是什麼?
Active Record 是具體的 ORM 實現,提供一種方式來定義和操作 Model,隱藏了資料庫操作的細節,允許開發人員使用物件導向語法來處理資料。
一般我們可以使用原始的 SQL 查詢尋找資料庫記錄,但在 Active Record 裡可以用他提供的方法來操作,
現在讓我們來看看在 Active Record 是如何搜尋資料的吧!
Active Record 提供了多種不同的方式來找尋單一物件(資料表中的一筆記錄)。
方法:find, find_by, first, last, take
find
find 方法,根據 主鍵 (id) 來查找單一記錄。user = User.find(1)
SELECT * FROM users WHERE id = 1;
find_by
find_by 方法,根據指定的條件查找第一個匹配的記錄。user = User.find_by(username: 'viii')
SELECT * FROM users WHERE username = 'viii' LIMIT 1;
find_by!與find_by非常相似,都是用來查找符合條件的記錄。但是,find_by!在找不到匹配的記錄時會引發ActiveRecord::RecordNotFound的錯誤訊息,而find_by僅返回nil。這可以用來強制確保查詢會找到一條記錄,並在未找到時引發錯誤訊息。
first
first 方法,檢索資料表中的第一條記錄。first_user = User.first
SELECT * FROM users LIMIT 1;
last
last 方法,檢索資料表中的最後一條記錄。last_user = User.last
SELECT * FROM users ORDER BY id DESC LIMIT 1;
take
take 方法,取得資料表中的一條記錄。user = User.take
SELECT * FROM users LIMIT 1;
當取得多筆資料時,很直覺覺得可以使用
all接著.each方法,但是!這樣可能會導致一次將整個資料表的資料提取出來,並存放在記憶體中,這樣的做法當資料量很大時,很容易超過記憶體的負荷。
  # 將整個資料表的資料全部加載到記憶體中
  User.all.each do |user|
    puts user.name
  end
為了避免這些問題,Active Record 提供 find_each 或 find_in_batches 方法,可以批次處理記錄,
而不是一次性將所有記錄載入記憶體,以便更有效地處理大量數據。這樣可以減輕記憶體負荷並提高效能。
方法:find_each, find_in_batches
find_eachfind_each 按批次檢索記錄,並遍歷紀錄。這對於處理大量記錄非常有用,因為每次只加載一批記錄,而不是全部加載到內存中。
  # 檢索所有用戶記錄,每次處理 1000 條記錄
User.find_each(batch_size: 1000) do |user|
  # 在這裡對每個用戶記錄進行操作
  puts user.name
end
Options for
find_each::batch_size,:start,:finish,:error_on_ignore,:order
:batch_size:指定每個批次中要檢索的記錄數量,用於控制每次處理的記錄數。
:start:配置序列的第一個 ID,可用於中斷的批次處理,指定從哪個 ID 開始。
:finish:配置序列的最後一個 ID,可用於檢索特定範圍內的記錄。
:error_on_ignore:用於控制當查詢操作遇到問題時應該發生什麼。如果在發現問題時立即中止操作,可以設置為 true;如果操作繼續進行,即使有問題也不中斷,可以保持默認值 false。
:order:指定主鍵 (id) 的順序,可以是升序 (:asc) 或降序 (:desc),默認為升序。
find_in_batchesfind_in_batches 方法類似於 find_each,按批次檢索記錄,但不提供遍歷功能。返回的是一個包含每批記錄的集合,可以透過這個集合並處理每批記錄。
# 檢索所有用戶記錄,每次處理 1000 條記錄
User.find_in_batches(batch_size: 1000) do |batch|
  # 在這裡處理每批記錄
  batch.each do |user|
    puts user.name
  end
end
Options for
find_in_batches::batch_size,:start,:finish,:error_on_ignore
這兩種方法都有助於減少大量數據查詢時的性能問題,特別是當記錄數量很龐大時,透過分批處理,可以更有效地管理記憶體使用,從而提高應用程式的效能。
where 方法接收一個條件可以使用 字串、陣列、物件(key : value)作為參數,並返回所有符合條件的查詢對象,是一個 ActiveRecord 查詢集合(Relation)。
如果找不到紀錄,會是一個空的 ActiveRecord 查詢結果 [ ]。
方法: where
Pure String Conditions 純字串條件
用戶可以通過輸入關鍵字來搜索產品的名稱:
keyword = params[:keyword] # 使用者輸入的搜索關鍵字
Product.where("name LIKE '%#{keyword}%'") # 不推薦的寫法,容易有 SQL injection 的風險
將使用者輸入的 keyword 直接插入到 SQL 字符中,這樣的做法可能導致 SQL injection!
SQL injection 是一個安全漏洞,攻擊者可以通過在 SQL 查詢中插入惡意代碼,從而對數據庫進行未授權的操作。
在 Rails 中,防止 SQL injection 的方式可以透過使用 Array Conditions(陣列條件)
Array Conditions 陣列條件
Product.where("name LIKE ?", params[:keyword]) # 推薦的寫法,使用 Array Conditions
陣列的第一個元素是一個 SQL 條件字串 "name LIKE ?",可以包含在 SQL 查詢的 WHERE 子句中。這個字串中可以包含佔位符,通常用問號 ? 來表示,Active Record 會將 ? 換成 params[:keyword] 做查詢,確保資料被安全地處理,以防止任何 SQL injection。
Placeholder Conditions
Placeholder conditions 具有類似於使用問號 ? 的 params 替換特性,這種風格通常用於傳遞參數值,以防止 SQL 注入攻擊。
除了使用問號之外,還可以在查詢條件字串中指定 keys/values 用 Hash 方式!這種方式的好處是,若條件中有許多參數,這種寫法不僅提高了可讀性,傳遞起來也更方便。
用戶可以搜索擁有特定名稱和年齡的用戶,使用 Placeholder conditions 來構建查詢條件:
conditions = "name = :user_name AND age > :min_age"
values = { user_name: "viii", min_age: 30 }
User.where(conditions, values)
# 寫在一起
User.where("name = :user_name AND age > :min_age", { user_name: "viii", min_age: 30 })
儘管條件參數會自動轉義以防止 SQL 注入,但 SQL LIKE 在遇到 SQL Wildcards (SQL 萬用字元),即
%和_下,不會轉義,這可能會導致意外行為。例如:
Book.where("title LIKE ?", params[:title] + "%")
可以通過 sanitize_sql_like 來解決:
Book.where("title LIKE ?", Book.sanitize_sql_like(params[:title]) + "%")
Hash Conditions
前情提要:
Only equality, range, and subset checking are possible with Hash conditions.
只有 Equality (相等性)、Range (範圍)、Subset (子集) 可用這種形式來寫條件。
Equality (相等性)
User.where(name: "viii")
SELECT * FROM users WHERE (users.name = 'viii')
'name' 指定為 "viii"。
User.where('name' => "viii")
User 有一個 belongs_to 關係,指向 Country 模型,country = Country.find_by(name: "USA")
User.where(country: country)
User 表具有複合主鍵,由 country_id 和 user_id 兩個列組成,User.where([:country_id, :user_id] => [[1, 101], [2, 202]])
Range (範圍)
Product.where(price: 10..50) # 返回所有價格在 10 到 50 之間的產品記錄。
SELECT * FROM products WHERE (products.price BETWEEN 10 AND 50)
Subset (子集)
User.where(name: ["Alice", "Bob", "Charlie"])
SELECT * FROM users WHERE (users.name IN ('Alice', 'Bob', 'Charlie'))
NOT, OR, AND Conditions
Active Record 提供了多種方法來構建 SQL 查詢的 NOT、OR 和 AND。
where.not
User.where.not(name: "John")
SELECT * FROM users WHERE NOT (users.name = 'John')
or
User.where(name: "Alice").or(User.where(name: "Bob"))
SELECT * FROM users WHERE (users.name = 'Alice' OR users.name = 'Bob')
where 條件,多個條件之間是 AND 關係。
User.where(name: "Alice", age: 25)
SELECT * FROM users WHERE (users.name = 'Alice' AND users.age = 25)
 User.order(age: :asc) # 升冪排序
 User.order(age: :desc) # 降冪排序
- 多次使用 `order`
```
User.order(:name).order(age: :desc)
```
生成的 SQL 查詢:`SELECT * FROM users ORDER BY users.name ASC, users.age DESC`
可以指定想要從資料庫中檢索的特定欄位,而不是檢索整個資料表,達到效能優化。
方法:select, distinct
- 假設有一個 `User` 模型,包含了 `name`、`email` 和 `age` ,但只想查 `name` 和 `email`:
    ```ruby
    User.select(:name, :email)
    ```
生成的 SQL 查詢:`SELECT name, email FROM users`
>要小心使用 `select`。因為實體化出來的物件僅有所選欄位。
>如果試圖存取不存在的欄位,會得到 ActiveModel::MissingAttributeError 異常:
>`ActiveModel::MissingAttributeError: missing attribute: <attribute>`
- 如果想要僅選擇某個欄位中每個唯一值對應的一條記錄,可以使用 `distinct` 
    ```ruby
    User.select(:name).distinct
    ```
生成的 SQL 查詢:`SELECT DISTINCT name FROM users`
Limit 和 Offset 是用於在 SQL 查詢中控制結果集大小和選擇的兩個重要參數。
- Limit(限制):Limit 用於限制查詢結果集的大小,指定了要返回的記錄數量。
- Offset(偏移):Offset 用於指定從查詢結果集的開頭位置開始返回記錄。允許跳過前幾條記錄,以便從指定位置開始檢索記錄。
繼續用 `User` 模型來講解,想查詢所有用戶的名字,但每次僅返回前五個名字,並且跳過前十個名字,這樣你可以從第十一個名字開始繼續查詢,這就可以使用 `limit` 和 `offset` 選項來實現這個目的:
```ruby
User.select(:name).limit(5).offset(10)
```
這個查詢將選擇 `User` 表中的 `name` 字段,然後使用 `limit(5)` 來指定返回前五個名字,使用 `offset(10)` 來指定從第十一個名字開始返回。
生成的 SQL 查詢:`SELECT name FROM users LIMIT 5 OFFSET 10`
>這兩個通常在分頁(pagination)中使用,在列表中顯示一部分記錄,然後允許用戶查看更多的記錄。
find、find_by、first、last、take。find_each 或 find_in_batches 以提高效能。where 方法可以建立條件搜尋,避免 SQL 注入攻擊。可以使用純字串、陣列、物件、或哈希來構建條件。 - NOT、OR、AND 條件也可輕鬆應用。order、select、distinct、limit 和 offset。瞭解這些方法可以更靈活地操作資料庫中的資料,今天先到這!明日再續,下篇見!
參考資料:
文章同步於個人部落格:Viiisit!(歡迎參觀 ୧ʕ•̀ᴥ•́ʔ୨)